package club.jdiy.core.sql;

import club.jdiy.core.convert.Converter;
import club.jdiy.core.ex.JDiyException;
import club.jdiy.utils.StringUtils;

import java.io.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Rs数据对象, 每一个Rs对应于查询结果集的一条记录.  <br /><br/>
 * <strong>特别注意：</strong><br />
 * 　　用户在调用Rs对象的相关get和set方法时，所传入的"field字段名称"参数,应该是查询结果集中显示的字段名称。
 * 例如下面的代码片段中,我们使用了列别名,我们应该始终用别名来作为相关get或set方法的方法参数传入.<br />
 * 示例代码片段如下:<br />
 * <div style="font-size:12px;">
 * ......<br/>
 * Args args = new Args("vip","username='oldname'", "id, username <b>As vipName</b>");<br />
 * Rs user = dao.rs(args);<br />
 * System.out.println("old username is:" + user.getConnection("<b>vipName</b>"));//读取字段值<br />
 * user.set("<b>vipName</b>", "newname"); //更新字段值<br /><span style="color:red">
 * //user.set("username", "newname");这种写法错误，应该使用了别名“vipName” </span>  <br />
 * System.out.println("new username is:" + user.getConnection("<b>vipName</b>"));   <br />
 * dao.save(user);//持久化到数据库<br />
 * ......<br />
 * </div>
 *
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20200125
 */

@SuppressWarnings({"unused"})
public class Rs extends LinkedHashMap<String, Object> implements Serializable {
    public static final long serialVersionUID = 1902885770180894886L;


    /**
     * 获取字段的非空字符串值.
     * 当字段值为空时,将返回空的字符串""而不是null(也就是说,此方法永远不会返回null).
     * 如果您不希望这种转换，请使用{@link #getString(String)}方法.
     * <br /><br />提示：<br />　　如果字段原数据类型不是String，系统将完成类型转换。
     *
     * @param field 字段名称(不区分大小写).
     * @return 该字段的字符串值.
     * @see #getString(String)
     */
    public Object get(String field) {
        return super.get(field);
    }

    public Object get(int index) {
        String[] keys = this.keySet().toArray(new String[0]);
        if (index > keys.length)
            throw new IndexOutOfBoundsException("index:" + index + " is out of bounds. MAX_VALUE= " + keys.length);
        return get(keys[index - 1]);
    }


    /**
     * 获取数据库字段的逻辑值.
     * <br /><br />提示：<br />　　如果字段原数据类型不是boolean型，系统将完成类型转换。
     *
     * @param field 要获取的字段名称(不区分大小写).
     * @return 该字段的逻辑值.
     */
    public Boolean getBoolean(String field) {
        return Converter.of(get(field)).getBoolean();
    }

    /**
     * 获取数据库字段的byte值.
     * 如果值为空, 将返回null.
     * <br /><br />提示：<br />　　如果字段原数据类型不是byte型，系统将完成类型转换。
     *
     * @param field 要获取的字段的名称(不区分大小写)。
     * @return 该字段的byte值.
     */
    public Byte getByte(String field) {
        return Converter.of(get(field)).getByte();
    }

    public Short getShort(String field) {
        return Converter.of(get(field)).getShort();
    }

    /**
     * 获取数据库字段的日期时间格式值. 如果数据库字段的值为空,将返回null.
     * <br /><br />提示：<br />　　如果字段原数据类型不是Date型，系统将完成类型转换。
     *
     * @param field 要获取的字段的名称(不区分大小写)。
     * @return java.util.Date 日期时间对象. <br />
     * 注意:返回的对象的实际数据类型有可能是java.util.Date的子类(例如:java.sql.Date, java.sql.Time),
     * 这主要由数据表中的所定义的日期时间类型(DATE, TIME, DATETIME, TIMESTAMP)来决定.
     */
    public Date getDate(String field) {
        return Converter.of(get(field)).getDate();
    }

    public LocalDate getLocalDate(String field) {
        return Converter.of(get(field)).getLocalDate();
    }

    public LocalDateTime getLocalDateTime(String field) {
        return Converter.of(get(field)).getLocalDateTime();
    }

    /**
     * 获取数据库字段的double值. 如果值这空,则返回null.
     * <br /><br />提示：<br />　　如果字段原数据类型不是double型，系统将完成类型转换。
     *
     * @param field 要获取的字段的名称(不区分大小写)。
     * @return 该字段的double值。
     */
    public Double getDouble(String field) {
        return Converter.of(get(field)).getDouble();
    }

    public BigDecimal getBigDecimal(String field) {
        return Converter.of(get(field)).getBigDecimal();
    }


    /**
     * 获取数据库字段的BigDecimal大数字值. 如果值这空,则返回null.
     * <br /><br />提示：<br />　　如果数据库字段原数据类型不是DECIMAL或者NUMBER型，系统将完成类型转换。
     *
     * @param field 要获取的字段的名称(不区分大小写)。
     * @return 该字段的BigDecimal值。
     * @see #getDouble(String)
     * @since 这是JDiy-1.11及后续版本新增的方法.
     */
    public BigDecimal getDecimal(String field) {
        return Converter.of(get(field)).getBigDecimal();
    }

    /**
     * 获取字段的Float值. 如果字段值为空,将返回null.
     * <br /><br />提示：<br />　　如果字段原数据类型不是float型，系统将完成类型转换。
     *
     * @param field 字段名称(不区分大小写).
     * @return Float 字段值
     */
    public Float getFloat(String field) {
        return Converter.of(get(field)).getFloat();
    }

    /**
     * 获取数据库字段的Integer值. 如果其值为空,将返回null.
     * <br /><br />提示：<br />　　如果字段原数据类型不是int型，系统将完成类型转换。
     *
     * @param field 要获取值的字段的名称(不区分大小写)。
     * @return 该字段的Integer值
     */
    public Integer getInt(String field) {
        return Converter.of(get(field)).getInteger();
    }


    /**
     * 获取数据库字段的Long值. 如果其值为空,将返回null.
     * <br /><br />提示：<br />　　如果字段原数据类型不是long型，系统将完成类型转换。
     *
     * @param field 要获取值的字段的名称(不区分大小写)。
     * @return 该字段的长整型值。
     */
    public Long getLong(String field) {
        return Converter.of(get(field)).getLong();
    }

    /**
     * 获取字段的字符串值.
     * 与{@link #get(String)}方法不同的是,当字段为空时,此方法将直接返回null, 而{@link #get(String)}方法会返回空的字符串"".
     * <br /><br />提示：<br />　　如果字段原数据类型不是String型，系统将完成类型转换。
     *
     * @param field 要获取值的字段名称(不区分大小写).
     * @return 该字段的字符串值.
     * @see #get(String)
     */
    public String getString(String field) {
        return Converter.of(get(field)).getString();
    }

    public String getString(int index) {
        return Converter.of(get(index)).getString();
    }

    /**
     * 获取数据库字段的meta信息. <br />
     *
     * @param field 要获取数据类型的字段名称(不区分大小写).
     * @return 返回值对应于java.sql.Types类的常量(它是一个整型数字). 或者返回null.
     */
    public ColumnInfo getColumnInfo(String field) {
        return columnInfoMap.get(field);
    }


    /**
     * 获取当前表的主键字段名.
     *
     * @return 注意是字段名, 不是主键的值, 若要获取主键的值,
     * 请使用: {@link #id()}或者  getLong(rs.getPrimaryKey()) 这样的形式.
     */
    public String getPrimaryKey() {
        return primaryKey;
    }

    /**
     * 获取当前数据的主键值.
     *
     * @return 当前数据行的主键值.
     */
    public String id() {
        return this.getString(getPrimaryKey());
    }


    /**
     * 判断当前的Rs对象是否为新创建的（即在底层数据库表中不存在对应的记录）.
     * <br/>
     * <br/>
     * <ul>
     *     <li>isNew=true时，表示该Rs在数据库中不存在。通过{@link Dao#save(Rs)}成功保存数据后，isNew将变为false</li>
     * <li>isNew=false时，表示数据库中有对应的记录，通过{@link Dao#del(Rs)}删除后，isNew将变为true</li>
     * <li><strong>注意:</strong> 在通过Dao的相关rs方法获取单条数据对象时,
     *      如果未查到记录,该方法并不会返回null,而是会创建一个isNew=true的新Rs对象返回，这时可以通过isNew()方法检测是否查到了记录.</li>
     * </ul>
     *
     * @return true 或 false
     */
    public boolean isNew() {
        return isNew;
    }

    public void ifPresent(Consumer<? super Rs> action) {
        if (!this.isNew) {
            action.accept(this);
        }
    }


    /**
     * 获取当前记录集对应的数据表名.
     * <br/>　　注意: JDiy将从查询返回的 ResultSet 结果集列元数据中从左往右取首个表的表名,
     * 对于那些没有映射表到结果集中的列（比如聚合和字面值）将自动跳过.
     * 如果至最后一个数据列,程序也未能取到表名,将将尝试从SQL语句中截获表名.
     * 有一种极端的情况，那就是查询的结果集列数据都是字面值(或聚合)值，
     * 且查询语句是一个子查询，程序将无法取到表名，在这种情况下，将返回null.
     * (另外，如果当前的数据库驱动提供程序并不支持从ResultSetMetaData对象中获取表名,此方法也可能返回null.)
     * <p>
     * <p>
     * <br /><strong>示例１：</strong><br/>
     * 　　有如下两个表的连接查询：<br/><em>select 123 as num, count(abc) as cnt, tb1.abc, tb2.xyz from tb1, tb2 where tb1.id=tb2.tb1_id group by abc, xyz</em><br/>
     * 　　其查结结果集列字段依次为： num, cnt, abc, bcd.
     * 程序从左往右取表名，其中第一列num为字面值(不属于任何表)跳过,第二列是一个聚合函结果值，也跳过，第三列abc是属于tb1的，
     * 则查询返回的Rs对象getTable()取到的表名将为 tb1.
     * <br/>
     * <br /><strong>示例2：</strong><br/>
     * 　　有如下子查询：<br/>
     * <em>select count(*) as cnt from (select id, title from table1) tb</em><br/>
     * 　　此查询仅有一个聚合列cnt（即结果列中不包括任何表的字段），且存在子查询．这种情况，返回的Rs记录， getTable()将返回null.
     *
     * @return 数据表名称.
     */
    public String getTable() {
        return this.table;
    }


    @Override
    public Object put(String key, Object value) {
        Object oldValue = get(key);
        boolean stringEq = oldValue != null && value instanceof String && oldValue.toString().equals(value);
        if (!Objects.equals(getPrimaryKey(), key)
                && !Objects.equals(value, oldValue)
                && !(stringEq)
                && columnInfoMap.containsKey(key)) changedColumns.add(key);
        return super.put(key, value);
    }

    @Override
    public void putAll(Map<? extends String, ?> m) {
        m.forEach(this::put);
    }

    @Override
    public Object remove(Object key) {
        changedColumns.remove(key.toString());
        return super.remove(key);
    }

    @Override
    public Object putIfAbsent(String key, Object value) {
        if (get(key) == null && columnInfoMap.containsKey(key)) changedColumns.add(key);
        return super.putIfAbsent(key, value);
    }

    @Override
    public boolean remove(Object key, Object value) {
        boolean b;
        if (b = super.remove(key, value)) changedColumns.remove(key.toString());
        return b;
    }

    @Override
    public boolean replace(String key, Object oldValue, Object newValue) {
        boolean b = super.replace(key, oldValue, newValue);
        if (b && columnInfoMap.containsKey(key)) changedColumns.add(key);
        return b;
    }

    @Override
    public Object replace(String key, Object value) {
        Object r = super.replace(key, value);
        if (r != null && columnInfoMap.containsKey(key)) changedColumns.add(key);
        return r;
    }

    @Override
    public Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) {
        if (get(key) == null && columnInfoMap.containsKey(key)) changedColumns.add(key);
        return super.computeIfAbsent(key, mappingFunction);
    }

    @Override
    public Object computeIfPresent(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
        if (get(key) != null && columnInfoMap.containsKey(key)) changedColumns.add(key);
        return super.computeIfPresent(key, remappingFunction);
    }

    @Override
    public Object compute(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
        if (columnInfoMap.containsKey(key)) changedColumns.add(key);
        return super.compute(key, remappingFunction);
    }

    @Override
    public Object merge(String key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) {
        if (columnInfoMap.containsKey(key)) changedColumns.add(key);
        return super.merge(key, value, remappingFunction);
    }

    @Override
    public void clear() {
        changedColumns.clear();
        super.clear();
    }

    @Override
    public void replaceAll(BiFunction<? super String, ? super Object, ?> function) {
        throw new JDiyException("Rs类不允许调用此方法。");
    }

    public Rs set(String field, Object value) {
        //fix: 当set空字符串进来时，目标列非字符串类型，就设置null或者忽略(不允许为null时忽略),
        // 若目标类型为字符串，则检查是否为外键，是外键设为null或者忽略(不允许为null时忽略):
        ColumnInfo ci;
        if ("".equals(value) && (ci = getColumnInfo(field)) != null) {
            switch (ci.getBaseType()) {
                case BIT:

                case TINYINT:
                case SMALLINT:
                case MEDIUMINT:
                case INT:
                case BIGINT:
                case FLOAT:
                case DOUBLE:
                case DECIMAL:

                case DATE:
                case TIME:
                case DATETIME:
                case TIMESTAMP:

                case OTHER:
                    if (ci.isNullable()) {
                        this.put(field, null);
                        return this;
                    }

                case VARCHAR:
                case CHAR:
                    if (ci.isNullable() && StringUtils.hasText(ci.getRefTableName())) {
                        this.put(field, null);
                        return this;
                    }
            }
        }

        this.put(field, value);
        return this;
    }


    /**
     * 返回Rs对象的字符串表示. <br/>
     * 其值格式为: 主键值+"@"+表名组成.如果是联合主键,多个主键值之间由冒号分隔.<br />
     * 例如:<br />
     * "y0gutsttqo@news"<br />
     * "3:abc@tablex"
     */
    @Override
    public String toString() {
        return getTable() + "{"+primaryKey+"=" + id() + "}";
    }

    private static String Encoding(char[] jz, long n) {
        long s = n / 36,
                y = n % 36;
        int sInt = (int) s,
                yInt = (int) y;
        if (s < 36) {
            return (sInt == 0 ? "" : String.valueOf(jz[sInt])) + jz[yInt];
        } else {
            return Encoding(jz, s) + jz[yInt];
        }
    }

    protected Rs(TableInfo tableInfo) {
        this.table = tableInfo.getTableName();
        this.columnInfoMap = tableInfo.getColumns();
        this.primaryKey = tableInfo.getPrimaryKey();
        this.isNew = true;
    }

    protected final Map<String, ColumnInfo> columnInfoMap;//列信息(表的全部)
    private final String table;
    private final String primaryKey;
    protected final Set<String> changedColumns = new HashSet<>();//修改过后的字段，isNew=false时，save只更新这些字段)
    protected boolean isNew;

}

